/**
 * TODO: This class deserves a better name, I just chose that cause I was out of ideas.
 */
package multimedia.model;

import java.awt.Dimension;
import java.awt.Point;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Scanner;
import java.util.Vector;

import magick.ColorspaceType;
import magick.ImageInfo;
import magick.MagickException;
import magick.MagickImage;
import multimedia.model.colorspace.ColorspaceInterface;
import multimedia.model.colorspace.ColorspaceUtil;
import multimedia.model.colorspace.DefaultColorspace;
import multimedia.model.colorspace.RGB;
import multimedia.util.ProjectConstants;

/**
 * @author trigueros
 * 
 */
public class ModifiedImage
{

    private MagickImage magickImage = null;
    private ImageInfo imageInfo = null;
    private Point regionOffset = null;
    private PixelCoordinate imagePixelHolder = null;
    private PixelCoordinate regionPixelHolder = null;
    private int colorspace;
    private Dimension imageSize;
    private int regionWidth;
    private int regionHeight;
    private Vector<ColorspaceInterface> modifiedPixelVector = null;
    private String originalFilename;
    private String binaryHeader = "0 0 0";

    public ModifiedImage()
    {
        imagePixelHolder = new PixelCoordinate();
    }

    /**
     * Initialize the {@link ModifiedImage} object and sets the image attribute.
     * 
     * @param imagePath
     *            absolute or relative path of the image to modify
     * @throws MagickException
     */
    public ModifiedImage(String imagePath) throws MagickException
    {
        // Initialize image data.
        imageInfo = new ImageInfo(imagePath);
        magickImage = new MagickImage(imageInfo);
        imageSize = magickImage.getDimension();
        magickImage.syncImage();
        regionOffset = new Point();
        
        // Super Hackish way to extract the filename.
        originalFilename = (new StringBuffer(( new StringBuffer(imagePath)).reverse().toString().split("/")[0] )).reverse().toString();
        
        // This will ensure that imagePixelHolder is not null
        selectRegion(new Point(0,0), new Point(imageSize.width, imageSize.height));
    }

    // Become obsolete Encoder.parseBinaryFile does this.
    @Deprecated
    public ModifiedImage(File binaryFile) throws FileNotFoundException
    {
        // Initialize stuff
//        magickImage = new MagickImage();
        
        originalFilename = binaryFile.getName();
        
        // Determine the format of the file read
        if( originalFilename.endsWith("YIQ") )
        {
            colorspace = ColorspaceType.YIQColorspace;
            // Then it should be parsed with YIQ
        }
        else if( originalFilename.endsWith("RGB") )
        {
            colorspace = ColorspaceType.RGBColorspace;
            // Then it should be parsed with RGB
        }
        else
        {
            throw new RuntimeException("Invalid Filetype");
        }
        
        // I don't know if it matters what kind of file it is
        imagePixelHolder = readFromBinaryFile(binaryFile);
        regionPixelHolder = imagePixelHolder;
    }
    
    /**
     * Obtain the pixels for the selected region in the image.
     * 
     * @param start
     * @param end
     * @return isValid if the selected region is valid, it'll return true
     */
    @SuppressWarnings("unchecked")
    public boolean selectRegion( Point start, Point end )
    {

        boolean isValid = false;
        regionOffset = start;
        // TODO: This may cause a problem if the region is not a rectangle, that is if regionWidth OR regionHeight is 0
        regionWidth = (end.x - start.x) +  0;
        regionHeight = (end.y - start.y) + 0;
        byte[] regionPixelBytes = new byte[regionWidth * regionHeight * 3];
        byte[] allPixelBytes = new byte[imageSize.width * imageSize.height * 3];

        try
        {
            magickImage.dispatchImage(regionOffset.x, regionOffset.y, regionWidth, regionHeight, "RGB",
                    regionPixelBytes);
            magickImage.dispatchImage(0, 0, imageSize.width, imageSize.height, "RGB", allPixelBytes);

            // Store the bytes into a handler object
            imagePixelHolder = new PixelCoordinate(allPixelBytes, imageSize.width);
            regionPixelHolder = new PixelCoordinate(regionPixelBytes, regionWidth);
            
            // Write something on modifiedPixelVector to avoid null pointer
            modifiedPixelVector = (Vector<ColorspaceInterface>) regionPixelHolder.getRgbVector().clone();

            isValid = true;
        } catch ( MagickException e )
        {
            System.err.println("Could not get pixels for selected region!");
            e.printStackTrace();
        }

        return isValid;
    }
    
    public void scaleImage(int row, int col)
    {
        try
        {
            magickImage = magickImage.scaleImage(row, col);
            setMagickImage(magickImage);
            setImageSize(new Dimension(row, col));
            selectRegion(new Point(0, 0), new Point(row, col));
        } catch ( MagickException e )
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Will compute the average of the color values and return a string with this information.
     * 
     * @param colorspace
     * @return the string composed of the averages.
     */
    public String computeRegionColorspacesAverage()
    {
        Vector<ColorspaceInterface> rgbVector = regionPixelHolder.getRgbVector();
        
        String averageOutput = "";
        
        averageOutput += "RGB Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage(rgbVector) + "\n\n";
        
        averageOutput += "XYZ Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToXYZ(rgbVector) ) + "\n\n";
        
        averageOutput += "Lab Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToLAB(rgbVector) ) + "\n\n";

        averageOutput += "YUV Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToYUV(rgbVector) ) + "\n\n";
        
        averageOutput += "YCbCr Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToYCbCr(rgbVector) ) + "\n\n";
        
        averageOutput += "YIQ Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToYIQ(rgbVector) ) + "\n\n";
        
        averageOutput += "HSL Average Color Values:\n";
        averageOutput += ColorspaceUtil.ComputeColorspaceAverage( ColorspaceUtil.ConvertToHSL(rgbVector) ) + "\n\n";
        
        return averageOutput;
    }

    /**
     * Increases all the components of all colorspaces by a constant value delta, and writes them to the output folder.
     * @param delta
     * @throws MagickException
     */
    @SuppressWarnings("unchecked")
    public void changeColorspacesByDelta( float delta ) throws MagickException
    {
        modifiedPixelVector = (Vector<ColorspaceInterface>) regionPixelHolder.getRgbVector().clone();
        
        // Increase RGB
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        ColorspaceUtil.NormalizeRGBValues(modifiedPixelVector);
        writeToFile("deltaRGB_" + originalFilename );
        
        // Increase XYZ
        modifiedPixelVector = ColorspaceUtil.ConvertToXYZ(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        writeToFile("deltaXYZ_" + originalFilename);
        
        // Increase Lab
        modifiedPixelVector = ColorspaceUtil.ConvertToLAB(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        writeToFile("deltaLab_" + originalFilename);
        
        // Increase YUV
        modifiedPixelVector = ColorspaceUtil.ConvertToYUV(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        writeToFile("deltaYUV_" + originalFilename);
        
        // Increase YCbCr
        modifiedPixelVector = ColorspaceUtil.ConvertToYCbCr(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        writeToFile("deltaYCbCr_" + originalFilename);
        
        // Increase YIQ and convert back to RGB
        modifiedPixelVector = ColorspaceUtil.ConvertToYIQ(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        modifiedPixelVector = ColorspaceUtil.ConvertFromYIQ(modifiedPixelVector);
        writeToFile("deltaYIQ_" + originalFilename);
        
        // Increase HSL
        modifiedPixelVector = ColorspaceUtil.ConvertToHSL(regionPixelHolder.getRgbVector());
        ColorspaceUtil.IncreaseByDelta(modifiedPixelVector, delta);
        writeToFile("deltaHSL_" + originalFilename);
    }
    
    @SuppressWarnings("unchecked")
    public void decreaseBitResolution() throws MagickException
    {
        String bitResPrefix = "bitResDecreased_";
        
        // Clone the Scaled Vector
        Vector<ColorspaceInterface> scaledVector = ColorspaceUtil.DecreaseBitResolution(regionPixelHolder.getRgbVector());
        
        // Create a low resolution RGB Vector
        modifiedPixelVector = (Vector<ColorspaceInterface>)scaledVector.clone();
        writeToFile(bitResPrefix + "rgb" + originalFilename);
        
        // Decrease Resolution for XYZ
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToXYZ(scaledVector)); 
        writeToFile(bitResPrefix + "xyz" + originalFilename);
        
        // Decrease Resolution for Lab
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToLAB(scaledVector)); 
        writeToFile(bitResPrefix + "lab"+ originalFilename);
        
        // Decrease Resolution for YUV
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToYUV(scaledVector)); 
        writeToFile(bitResPrefix + "yuv" + originalFilename);
        
        // Decrease Resolution for YCbCr
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToYCbCr(scaledVector)); 
        writeToFile(bitResPrefix + "ycbcr" + originalFilename);
        
        // Decrease Resolution for YIQ, and converting back to RGB
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToYIQ(scaledVector));
        modifiedPixelVector = ColorspaceUtil.ConvertFromYIQ(modifiedPixelVector);
        writeToFile(bitResPrefix + "yiq" + originalFilename);
        
        // Decrease Resolution for HSL
        modifiedPixelVector = ColorspaceUtil.DecreaseBitResolution(ColorspaceUtil.ConvertToHSL(scaledVector)); 
        writeToFile(bitResPrefix + "hsl" + originalFilename);
    }
    
    @SuppressWarnings("unchecked")
    public void changeBrightness( int percent )
    {
        modifiedPixelVector = ColorspaceUtil.IncreaseBrightnessByPercentage((Vector<ColorspaceInterface>) regionPixelHolder.getRgbVector().clone(), percent);
    }
    
    @SuppressWarnings("unchecked")
    public void changeSaturation( int percent )
    {
        modifiedPixelVector = ColorspaceUtil.IncreaseSaturationByPercentage((Vector<ColorspaceInterface>) regionPixelHolder.getRgbVector().clone(), percent);
    }
    
    /**
     * Writes the modified image data into an image in the following directory:
     * ./imagemagick_out/
     * @param filename name chosen for image
     * @throws MagickException
     */
    public void writeToFile( String filename ) throws MagickException
    {
        // Create full filename
        String filePath = ProjectConstants.IMAGE_OUTDIR + filename;
        ImageInfo newInfo = new ImageInfo(filePath);
        // Setting up the colorspace
//        newInfo.setColorspace(currentColorspace);

        // Applying any changes to the new image.
        MagickImage newMagickImage = new MagickImage(newInfo);
        applyChanges(newMagickImage);

        newMagickImage.setFileName(filePath);
        newMagickImage.writeImage(newInfo);
    }

    /**
     * From the already read file, we can create a file containing just the byte values so 
     * that they can be read w/o the need for ImageMagick
     * @param filename
     */
    public void writeToBinaryFile( String filename , String fileOutput)
    {
        try
        {
            FileWriter fileStream = new FileWriter(filename);
            BufferedWriter writer = new BufferedWriter(fileStream);
            
            // Write information about the file that we'll be writing
            writer.write( imageSize.width + " " + imageSize.height + "\n");
            
            // Append the binary header information
            writer.write(binaryHeader + "\n");
            
            // Write the pixels onto the file, and do a new
//            for ( ColorspaceInterface rgb : imagePixelHolder.getRgbVector() )
//            {
//                writer.write( rgb.getFirstComponent() + " ");
//                writer.write( rgb.getSecondComponent() + " ");
//                writer.write( rgb.getThirdComponent() + " ");
//            }
            writer.write(fileOutput);
            writer.write("\n");
            
            writer.close();
            
        } catch ( IOException e )
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    public void writeBinaryToImage( String filename ) throws MagickException
    {
        String filePath = ProjectConstants.IMAGE_OUTDIR + filename;
        ImageInfo newInfo = new ImageInfo(filePath);
        
        MagickImage newMagickImage = new MagickImage(newInfo);
        
        byte[] finalizedImageData =  ColorspaceUtil.ConvertToByteArray(imagePixelHolder.getRgbVector(), imageSize.width, imageSize.height);
        
        newMagickImage.constituteImage(imageSize.width, imageSize.height, "RGB", finalizedImageData);
        newMagickImage.setFileName(filePath);
        newMagickImage.writeImage(newInfo);
    }
    
    /***
     * This method needs to go somewhere else.
     * @param binaryFile
     * @return
     * @throws FileNotFoundException
     */
    @Deprecated
    private PixelCoordinate readFromBinaryFile( File binaryFile ) throws FileNotFoundException
    {
        Scanner fileReader = new Scanner(binaryFile);
        
        // The first line has the dimensions
        String[] dimensionLine = fileReader.nextLine().split(" ");
        
        // Set the dimensions
        imageSize = new Dimension( Integer.parseInt(dimensionLine[0]), Integer.parseInt(dimensionLine[1]) );
        
        /*
         * This is a big TODO, I don't know how we are going to go about determining what to do with the information
         * if you guys have any suggestions, this is where you would be putting it. I guess I'll lay out some footwork
         * then we can go from there
         */
        // TODO: Add decoding instructions
        // The second line has decoding instructions
        String[] encodingParameters = fileReader.nextLine().split(" ");
        
        // Set Quantization 
        Integer.parseInt(encodingParameters[0]);
        
        // Set the Predictor and perform decoding
//        Predictor predictor = new Predictor(this, Integer.parseInt(encodingParameters[1]));
        
        // Set Encoding
        Integer.parseInt(encodingParameters[2]);
        
        
        // If this crashes it's programmer error
        // Create a ColorspaceVector
        Vector<ColorspaceInterface> binaryFileVector = new Vector<ColorspaceInterface>();
        try
        {
            while( fileReader.hasNext() )
            {
                // Create dummy pixel holder 
                DefaultColorspace currentPixel = new DefaultColorspace();
                
                // Get the next three values from the file
                currentPixel.setFirstComponent(  (float) Double.parseDouble( fileReader.next() ) );
                currentPixel.setSecondComponent( (float) Double.parseDouble( fileReader.next() ) );
                currentPixel.setThirdComponent(  (float) Double.parseDouble( fileReader.next() ) );
                
                // Insert that new pixel into the vector
                binaryFileVector.add( currentPixel );
            }
        } catch ( NullPointerException e )
        {
            System.err.println("If this crashes, it's programmer error, this is tool. Do it right.");
            e.printStackTrace();
        }
        
       return new PixelCoordinate(binaryFileVector, imageSize.width);
    }
    
    @SuppressWarnings("unchecked")
    private void applyChanges(MagickImage image) throws MagickException
    {
        // Copy the original picture onto another vector
        PixelCoordinate oldPixels = new PixelCoordinate((Vector<ColorspaceInterface>)imagePixelHolder.getRgbVector().clone(),
                imageSize.width);
        PixelCoordinate newPixels = new PixelCoordinate(modifiedPixelVector, regionWidth);


        for ( int y = 0; y < regionHeight; y++ )
        {
            for ( int x = 0; x < regionWidth; x++ )
            {
                ColorspaceInterface currentPixel = oldPixels.getPixelAt(x + regionOffset.x, y + regionOffset.y);
                currentPixel.setFirstComponent( newPixels.getPixelAt(x, y).getFirstComponent() );
                currentPixel.setSecondComponent( newPixels.getPixelAt(x, y).getSecondComponent() );
                currentPixel.setThirdComponent( newPixels.getPixelAt(x, y).getThirdComponent() );
            }
        }
        
        Vector<ColorspaceInterface> newPixel = oldPixels.getRgbVector();

        byte[] finalizedImageData =  ColorspaceUtil.ConvertToByteArray(newPixel, imageSize.width, imageSize.height);
        
        image.constituteImage(imageSize.width, imageSize.height, "RGB", finalizedImageData);
    }

    public RGB calculateDistortion( ModifiedImage original )
    {
        RGB totalDistortion = new RGB();
        
        for( int pixelIndex = 0 ; pixelIndex < this.getImagePixelHolder().getRgbVector().size(); pixelIndex++ )
        {
            RGB temp = new RGB();
            RGB a = new RGB(this.getImagePixelHolder().getRgbVector().get(pixelIndex));
            RGB b = new RGB(original.getImagePixelHolder().getRgbVector().get(pixelIndex));
            
            temp = b.subtract(a);
            temp.square();
            
            totalDistortion = totalDistortion.add(temp);
        }
        return totalDistortion.divideBy(imageSize.width*imageSize.height);
    }
    
    public void setMagickImage( MagickImage magickImage )
    {
        this.magickImage = magickImage;
    }
    
    public MagickImage getMagickImage()
    {
        return magickImage;
    }

    /**
     * @return the imageSize
     */
    public Dimension getImageSize()
    {
        return imageSize;
    }

    public void setImageSize(Dimension size)
    {
        imageSize = size;
    }
    
    /**
     * @return the imagePixelHolder
     */
    public PixelCoordinate getImagePixelHolder()
    {
        return imagePixelHolder;
    }

    /**
     * @param imagePixelHolder the imagePixelHolder to set
     */
    public void setImagePixelHolder( PixelCoordinate imagePixelHolder )
    {
        this.imagePixelHolder = imagePixelHolder;
    }

    /**
     * @return the regionPixelHolder
     */
    public PixelCoordinate getRegionPixelHolder()
    {
        return regionPixelHolder;
    }

    /**
     * @param regionPixelHolder the regionPixelHolder to set
     */
    public void setRegionPixelHolder( PixelCoordinate regionPixelHolder )
    {
        this.regionPixelHolder = regionPixelHolder;
    }

    public int getColorspace()
    {
        return colorspace;
    }

    public void setColorspace( int colorspace )
    {
        this.colorspace = colorspace;
    }

    /**
     * @return the binaryHeader
     */
    public String getBinaryHeader()
    {
        return binaryHeader;
    }

    /**
     * @param binaryHeader the binaryHeader to set
     */
    public void setBinaryHeader( String binaryHeader )
    {
        this.binaryHeader = binaryHeader;
    }

	/**
	 * @return
	 */
	public String getOriginalFilename() {
		return originalFilename;
	}
	
	public void setOriginalFilename( String name )
	{
	    originalFilename = name;
	}
	
	public void setRegionDimension( int width, int height )
	{
	    regionWidth = width;
	    regionHeight = height;
	}
}
